﻿//_______________________________________________________________________________________________________________________
// Copy Right (c) 2010, 2011 SapiensIT Consulting GmbH, all rights reserved
//_______________________________________________________________________________________________________________________
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT
// NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
//_______________________________________________________________________________________________________________________
// PROJECT:     sapiens.at.SharePoint (SharePoint 2010/V4)
//_______________________________________________________________________________________________________________________
// VERSION:     2.0
//_______________________________________________________________________________________________________________________
// DESCRIPTION: 
//_______________________________________________________________________________________________________________________
// LICENSE:     Different licens Types apply to different file types:
//               - Source code files are governed by the MICROSOFT PUBLIC LICENSE (Ms-PL)
//               - Binary files are governed by MSDN CODE GALLERY BINARY LICENSE
//               - Documentation files are governed by CREATIVE COMMONS ATTRIBUTION 3.0 LICENSE
//
//               The licenses can be found at the projects source location.
//               If you do not agree, don't use it!
// 
//_______________________________________________________________________________________________________________________
// Start SharePoint Branding, visit http://www.cliggin.com! Visit us on http://www.sapiens.at! Tell your friends! 
//_______________________________________________________________________________________________________________________
using System;
using System.Linq;
using System.Collections.Generic;
using Microsoft.SharePoint;
using System.Globalization;
using Sapiens.at.SharePoint.Utils;
using QB = Sapiens.at.SharePoint.Utils.QueryBuilder;
using Sapiens.at.SharePoint.Security;
using Sapiens.at.SharePoint.Extensions;
using Sapiens.at.SharePoint.Repositories;
using Sapiens.at.SharePoint.Receiver;
using Microsoft.SharePoint.Utilities;
using Sapiens.at.Core.Utils;

namespace Sapiens.at.SharePoint.Entities
{
    /// <summary>
    /// Business entity that represents a <see cref="SPListItem"/>.
    /// </summary>
    public abstract class Entity : IEntity
    {
        #region Constants
        /// <summary>
        /// 0x
        /// </summary>
        public const string CTYPE_ID = "0x";

        /// <summary>
        /// 0x
        /// </summary>
        public static readonly SPContentTypeId CTypeId = new SPContentTypeId(CTYPE_ID);

        /// <summary>
        /// ID
        /// </summary>
        public const string ID_ = "ID";
        
        /// <summary>
        /// UniqueId
        /// </summary>
        public const string UNIQUE_ID = "UniqueId";

        /// <summary>
        /// ContentTypeId
        /// </summary>
        public const string CONTENTTYPEID = "ContentTypeId";

        /// <summary>
        /// ContentType
        /// </summary>
        public const string CONTENTTYPE = "ContentType";

        /// <summary>
        /// Title
        /// </summary>
        public const string TITLE = "Title";

        /// <summary>
        /// LinkTitle
        /// </summary>
        public const string LINK_TITLE = "LinkTitle";

        /// <summary>
        /// LinkTitleNoMenu
        /// </summary>
        public const string LINK_TITLE_NO_MENU = "LinkTitleNoMenu";

        /// <summary>
        /// FileLeafRef
        /// </summary>
        public const string FILELEAFREF = "FileLeafRef";

        /// <summary>
        /// Editor
        /// </summary>
        public const string EDITOR = "Editor";

        /// <summary>
        /// Author
        /// </summary>
        public const string AUTHOR = "Author";

        /// <summary>
        /// FSObjType
        /// </summary>
        public const string FSOBJTYPE = "FSObjType";

        /// <summary>
        /// Inverse-{0}-{1}
        /// </summary>
        public const string INVERSE_LOOKUP_KEY = "Inverse-{0}-{1}";
        #endregion

        #region Fields

        private bool _loaded;
        private int _id;
        private string _title;
        private SPListItem _listItem;
        private SPItemEventProperties _eventProperties;
        private SPList _list;
        private SPWeb _web;

        /// <summary>
        /// A cache storing the values of the fields
        /// </summary>
        private Dictionary<string, object> _fieldsCache;

        /// <summary>
        /// For tracking the fields that have been modified
        /// </summary>
        private List<string> _modifiedFields;

        /// <summary>
        /// Stores already fetched and initialized lookup entities to increase performance.
        /// </summary>
        private Dictionary<string, IEntity> _lookupCache;

        /// <summary>
        /// Stores already fetched and initialized multi-lookup entities to increase performance.
        /// </summary>
        private Dictionary<string, IEnumerable<IEntity>> _multiLookupCache;
        #endregion

        #region Properties

        /// <summary>
        /// Gets the ID of the listitem.
        /// </summary>
        /// <remarks>
        /// This property works in unloaded state.
        /// </remarks>
        public int Id
        {
            get
            {
                if (_id <= 0 && ListItem != null)
                {
                    _id = int.Parse(Convert.ToString(ListItem["ID"]));
                }
                return _id;
            }
        }

        /// <summary>
        /// Gets or sets the unique ID
        /// </summary>
        public Guid Unique_ID
        {
            get { return new Guid(Get<string>(UNIQUE_ID)); }
            set { Set(UNIQUE_ID, value); }
        }

        /// <summary>
        /// Gets the object type
        /// </summary>
        public string FSObjType
        {
            get { return Get<string>(FSOBJTYPE); }
        }

        /// <summary>
        /// Gets the fileleafref
        /// </summary>
        public string FileLeafRef
        {
            get { return Get<string>(FILELEAFREF); }
        }

        /// <summary>
        /// Gets or sets the contenttype id
        /// </summary>
        /// <remarks>
        /// If the content type is changed, the entity is still of the same type as before.
        /// </remarks>
        public SPContentTypeId ContentTypeId
        {
            get
            {
                var cTypeId = Get<string>(CONTENTTYPEID);
                if (string.IsNullOrEmpty(cTypeId))
                { // no contenttype found in the listitem or afterproperties -> take first of the list
                    return List.ContentTypes[0].Id;
                }
                return new SPContentTypeId(cTypeId);
            }
            set { Set(CONTENTTYPEID, value.ToString()); }
        }

        /// <summary>
        /// Gets or sets the contenttype name
        /// </summary>
        public string ContentType
        {
            get { return Get<string>(CONTENTTYPE); }
            set { Set(CONTENTTYPE, value); }
        }

        /// <summary>
        /// Gets or sets the Title of the listitem.
        /// </summary>
        /// <remarks>
        /// This property works in unloaded state. Calling the setter will trigger the <see cref="LazyLoad()"/>.
        /// </remarks>
        public string Title
        {
            get { return _loaded || string.IsNullOrEmpty(_title) ? Get<string>(TITLE) : _title; }
            set { Set(TITLE, value); }
        }

        /// <summary>
        /// Gets the author
        /// </summary>
        public SPUser Author
        {
            get { return (SPUser)GetUserOrGroup(AUTHOR); }
            set { SetUserOrGroup(AUTHOR, value); }
        }

        /// <summary>
        /// Gets the editor
        /// </summary>
        public SPUser Editor
        {
            get { return (SPUser)GetUserOrGroup(EDITOR); }
            set { SetUserOrGroup(EDITOR, value); }
        }

        /// <summary>
        /// Gets the listitem that this entity represents.
        /// </summary>
        public SPListItem ListItem
        {
            get
            {
                LazyLoad();
                return _listItem;
            }
        }

        private SPListItem _parentItem;
        /// <summary>
        /// Gets the parentItem if the <see cref="ListItem"/> is in a folder. Otherwise null.
        /// </summary>
        public SPListItem ParentItem
        {
            get
            {
                if (_parentItem == null)
                {
                    if (ListItem == null || _web == null || !ListItem.Url.Contains("/")) return null;
                    SPFolder parent = _web.GetFolder(ListItem.Url.Substring(0, ListItem.Url.LastIndexOf("/")));
                    if (!parent.Exists) return null;
                    _parentItem = parent.Item;
                }
                return _parentItem;
            }
        }

        /// <summary>
        /// Gets the list of this entity.
        /// </summary>
        public SPList List
        {
            get { return _list; }
        }

        /// <summary>
        /// Gets the web of this entity.
        /// </summary>
        public SPWeb Web
        {
            get { return _web; }
        }

        /// <summary>
        /// Is true, if this entity has been created during a synchronous item event
        /// </summary>
        protected bool IsSynchronous
        {
            get { return _eventProperties != null && _eventProperties.IsSynchronousEvent(); }
        }

        /// <summary>
        /// Returns true, if any of the fields got modified; otherwise false.
        /// This is not intended to tell whether the user has changed fields in the edit form.
        /// </summary>
        /// <remarks>
        /// Returns false, if the entity is in unloaded state.
        /// </remarks>
        public bool IsModified
        {
            get { return _loaded && _modifiedFields.Count > 0; }
        }

        /// <summary>
        /// Gets a list of the fieldnames of the changed fields.
        /// </summary>
        /// <remarks>
        /// If the entity is in unloaded state it returns null.
        /// </remarks>
        public IEnumerable<string> ModifiedFields
        {
            get { return _loaded ? _modifiedFields : null; }
        }

        private Dictionary<string, IEntity> LookupCache
        {
            get
            {
                if (_lookupCache == null)
                {
                    _lookupCache = new Dictionary<string, IEntity>();
                }
                return _lookupCache;
            }
        }

        private Dictionary<string, IEnumerable<IEntity>> MultiLookupCache
        {
            get
            {
                if (_multiLookupCache == null)
                {
                    _multiLookupCache = new Dictionary<string, IEnumerable<IEntity>>();
                }
                return _multiLookupCache;
            }
        }

        #endregion

        #region Constructors
        /// <summary>
        /// Instantitates an entity. Never call this, except from a parameterless constructor of a derived entity class.
        /// </summary>
        protected Entity() { }

        /// <summary>
        /// Instantiates an entity using the id and the title.
        /// </summary>
        /// <remarks>
        /// The entity gets instantiated in unloaded state. Call <see cref="LazyLoad(SPList)"/> to load the entity later or use <see cref="Entity(int, string, SPList)"/> instead.
        /// </remarks>
        /// <seealso cref="LazyLoad()"/>
        /// <param name="id">The id of the item</param>
        /// <param name="title">The title of the item</param>
        public Entity(int id, string title)
            :this()
        {
            _id = id;
            _title = title;
            _loaded = false;
        }

        /// <summary>
        /// Instantiates an entity using the id and the title.
        /// </summary>
        /// <remarks>
        /// The entity gets instantiated in unloaded state and loads the listitem on first usage.
        /// </remarks>
        /// <seealso cref="LazyLoad()"/>
        /// <param name="id">The id of the item</param>
        /// <param name="title">The title of the item</param>
        /// <param name="list">The list of the item</param>
        public Entity(int id, string title, SPList list)
            : this(id, title)
        {
            Validation.ArgumentNotNull(list, "list");

            _list = list;
            _web = list.Lists.Web;
        }

        /// <summary>
        /// Instantiates an entity using the eventproperties
        /// </summary>
        /// <param name="web">SPWeb</param>
        /// <param name="eventProperties"></param>
        public Entity(SPItemEventProperties eventProperties, SPWeb web)
            : this()
        {
            Validation.ArgumentNotNull(eventProperties, "eventProperties");
            var tWeb = web ?? eventProperties.Web;
            Validation.ArgumentNotNull(tWeb, "web");
            
            var list = tWeb.Lists[eventProperties.ListId];
            var listItem = eventProperties.ListItem;
            if (listItem != null && web != null && eventProperties.Web != web)
            {
                listItem = list.GetItemByUniqueId(listItem.UniqueId);
            }
            Load(listItem, eventProperties, list, tWeb);
        }

        /// <summary>
        /// Instantiates an entity using the listitem
        /// </summary>
        /// <param name="listItem"></param>
        public Entity(SPListItem listItem)
            : this()
        {
            Validation.ArgumentNotNull(listItem, "listItem");

            Load(listItem, null, listItem.ParentList, listItem.Web);
        }

        /// <summary>
        /// Instantiates an entity using a different listitem as in the eventproperties (i.e. if the listitem is privileged)
        /// </summary>
        /// <param name="web"></param>
        /// <param name="eventProperties"></param>
        /// <param name="listItem"></param>
        public Entity(SPItemEventProperties eventProperties, SPListItem listItem, SPWeb web)
            :this()
        {
            Validation.ArgumentNotNull(eventProperties, "eventProperties");
            //Validation.ArgumentNotNull(listItem, "listItem");

            var list = listItem != null ? listItem.ParentList : web.Lists[eventProperties.ListId];
            Load(listItem, eventProperties, list, web ?? listItem.Web);
        }
        #endregion

        #region Initialization Methods
        /// <summary>
        /// Helper method to provide a central point to set the neccessary fields.
        /// </summary>
        private void Load(SPListItem listItem, SPItemEventProperties eventProperties, SPList list, SPWeb web)
        {
            _id = listItem != null ? listItem.ID : 0;
            _listItem = listItem;
            _eventProperties = eventProperties;
            _list = list;
            _web = web;
            _loaded = true;
            Initialize();
        }

        /// <summary>
        /// If the entity is in unloaded state, fetches the <see cref="SPListItem"/> for this entity using it's <see cref="List"/> and <see cref="Id"/>.
        /// </summary>
        /// <remarks>
        /// The <see cref="List"/> has to be set prior to calling this method. Use <see cref="LazyLoad(SPList)"/> instead to set the list and load the entity.
        /// </remarks>
        /// <exception cref="InvalidOperationException">An InvalidOperationException is thrown, if the list has not been initialized.</exception>
        /// <exception cref="ArgumentNullException">An ArgumentNullException is thrown, if the listItem can't be found.</exception>
        protected virtual void LazyLoad()
        {
            if (_loaded) { return; }
            if (_list == null || _web == null) { throw new InvalidOperationException("Entity was not set up correctly. Set _list and _web before initializing."); }

            LazyLoad(_list.GetItemById(Id));
        }

        /// <summary>
        /// If the entity is in unloaded state, sets this' <see cref="SPListItem"/>.
        /// </summary>
        /// <param name="listItem">The listItem, this entity represents</param>
        /// <exception cref="ArgumentNullException">An ArgumentNullException is thrown, if the passed listItem is null.</exception>
        protected virtual void LazyLoad(SPListItem listItem)
        {
            if (_loaded) { return; }
            Validation.ArgumentNotNull(listItem, "listItem");

            _listItem = listItem;
            if (_list == null) { _list = listItem.ParentList; }
            if (_web == null) { _web = listItem.Web; }

            _loaded = true;
            Initialize();
        }

        /// <summary>
        /// If the entity is in unloaded state, fetches the <see cref="SPListItem"/> for this entity using it's <see cref="List"/> and <see cref="Id"/>.
        /// </summary>
        /// <param name="list">The list of this item</param>
        /// <exception cref="ArgumentNullException">An ArgumentNullException is thrown, if the passed list is null.</exception>
        protected virtual void LazyLoad(SPList list)
        {
            Validation.ArgumentNotNull(list, "list");

            _list = list;
            _web = list.Lists.Web;
            
            LazyLoad();
        }

        /// <summary>
        /// Initialization method that gets called as soon as the entity gets loaded.
        /// </summary>
        protected virtual void Initialize()
        {
            _fieldsCache = new Dictionary<string, object>();
            _modifiedFields = new List<string>();
        }
        #endregion

        #region Field Methods

        /// <summary>
        /// Returns the value for the given field.
        /// </summary>
        /// <remarks>
        /// If this item is in the context of a synchronous event, the method first tries to get the value from the after-properties.
        /// Only if there is no such property it returns the value from the SPListItem object. This mimics an asynchronous behaviour in a synchronous event.
        /// The resulting value is stored in a cache for performance optimization.
        /// </remarks>
        /// <typeparam name="T">The type of the result</typeparam>
        /// <param name="fieldName">the name of the field</param>
        /// <returns>the typecasted value</returns>
        protected internal T Get<T>(string fieldName)
        {
            if (!_loaded) { LazyLoad(); }
            if (_fieldsCache.ContainsKey(fieldName)) { return (T)_fieldsCache[fieldName]; }

            object fieldValue = null;
            if (_eventProperties != null && _eventProperties.IsSynchronousEvent())
            {   // inside a synchronous event
                if (_eventProperties.AfterProperties[fieldName] != null)
                { // try to find a value in the afterproperties
                    fieldValue = _eventProperties.AfterProperties[fieldName];
                }
                else if (ListItem != null)
                { // try to find it in the listitem (no listitem during ItemAdding)
                    fieldValue = ListItem[fieldName];
                }
            }
            else
            {
                fieldValue = ListItem[fieldName];
            }
            // convert to propert type
            var result = Conversion.SafeCast<T>(fieldValue);
            _fieldsCache.Add(fieldName, result);
            return result;
        }

        /// <summary>
        /// Sets a property if it has changed.
        /// </summary>
        /// <remarks>
        /// It only changes the property of the ListItem/After-properties if the new value is different to the current one.
        /// If the field got changed, it is marked as modified in the <see cref="ModifiedFields"/> collection.
        /// </remarks>
        /// <typeparam name="T">The type of the value</typeparam>
        /// <param name="fieldName">The name of the field to be changed</param>
        /// <param name="value">The value to be assigned</param>
        /// <returns>true, if the value is different to the old one; otherwise false</returns>
        protected internal bool Set<T>(string fieldName, T value)
        {
            // ME: should be fine
            //object temp = value;
            //if (typeof(T) == typeof(double) || typeof(T) == typeof(decimal) || typeof(T) == typeof(float))
            //{
            //    temp = Convert.ToString(value, CultureInfo.InvariantCulture);
            //}
            var currentValue = Get<T>(fieldName);
            bool changed = (value == null && currentValue != null) || (value != null && !value.Equals(Get<T>(fieldName)));
            if (changed) { DoSet(fieldName, value); }
            return changed;
        }

        /// <summary>
        /// Sets a datetime property if it has changed. See <see cref="Set{T}"/>.
        /// </summary>
        /// <param name="fieldName"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        protected internal bool Set(string fieldName, DateTime value)
        {
            return Set(fieldName, SPUtility.CreateISO8601DateTimeFromSystemDateTime(value));
        }

        // REVIEW: Do we need a separate method? Probably integrate in Set<T>()
        private void DoSet(string fieldName, object value)
        {
            if (_eventProperties != null && _eventProperties.IsSynchronousEvent() && _eventProperties.EventType != SPEventReceiverType.ItemDeleting)
            {
                //if (value is SPFieldLookupValueCollection)
                //{
                //    _eventProperties.AfterProperties[fieldName] = value.ToString();
                //}
                //else
                //{
                //    _eventProperties.AfterProperties[fieldName] = value;
                //}
                _eventProperties.AfterProperties[fieldName] = Convert.ToString(value, CultureInfo.InvariantCulture);
            }
            else
            {
                //ListItem[fieldName] = value;s
                ListItem[fieldName] = Convert.ToString(value, CultureInfo.InvariantCulture);
            }
            _fieldsCache[fieldName] = value;
            // register field as modified
            if (!_modifiedFields.Contains(fieldName)) { _modifiedFields.Add(fieldName); }
        }

        #endregion

        #region Lookup Methods
        /// <summary>
        /// Returns the ID of a lookup field
        /// </summary>
        /// <param name="fieldName">The name of the lookup field</param>
        /// <returns></returns>
        protected internal int GetLookupId(string fieldName)
        {
            return new SPFieldLookupValue(Get<string>(fieldName)).LookupId;
        }

        /// <summary>
        /// Returns the entity to which a lookup field refers.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity to return</typeparam>
        /// <param name="fieldName">The name of the lookup field</param>
        /// <returns>A new instance of <typeparamref name="TEntity"/> representing the listitem to which the lookup field refers</returns>
        protected internal TEntity GetLookup<TEntity>(string fieldName)
            where TEntity : IEntity
        {
            return GetLookup<TEntity>(fieldName, Get<SPFieldLookupValue>(fieldName));
        }

        /// <summary>
        /// Returns the entity to which a lookup field refers.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity to return</typeparam>
        /// <param name="fieldName">The name of the lookup field</param>
        /// <param name="lookupValue">The lookup value of the corresponding lookup field</param>
        /// <returns>A new instance of <typeparamref name="TEntity"/> representing the listitem to which the lookup field refers</returns>
        protected internal TEntity GetLookup<TEntity>(string fieldName, SPFieldLookupValue lookupValue)
            where TEntity : IEntity
        {
            if (lookupValue == null || lookupValue.LookupId == 0) { return default(TEntity); }

            // Check the lookup cache for this lookup
            if (LookupCache.ContainsKey(fieldName) && LookupCache[fieldName].Id == lookupValue.LookupId)
            {
                return (TEntity)LookupCache[fieldName];
            }

            // Get the list to which the lookup field points
            var lookupList = GetLookupList(fieldName);
            var entity = ListItemMapper.GetEntity<TEntity>(lookupValue.LookupId, lookupValue.LookupValue, lookupList);
            
            // store the new entity in the lookup cache
            LookupCache[fieldName] = entity;
            return entity;
        }

        /// <summary>
        /// Helper method to get the Entities that a lookup field points to.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity to return</typeparam>
        /// <param name="fieldName">The name of the lookup field</param>
        /// <returns></returns>
        protected internal IEnumerable<TEntity> GetLookupItems<TEntity>(string fieldName)
            where TEntity : IEntity
        {
            Validation.ArgumentNotNullOrEmpty(fieldName, "fieldName");

            return GetLookupItems<TEntity>(fieldName, Get<SPFieldLookupValueCollection>(fieldName));
        }

        /// <summary>
        /// Helper method to get the Entities that a lookup field points to.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity to return</typeparam>
        /// <param name="fieldName">The name of the lookup field</param>
        /// <param name="lookupValues">The lookupvalues to get the entities for</param>
        /// <returns></returns>
        protected internal IEnumerable<TEntity> GetLookupItems<TEntity>(string fieldName, SPFieldLookupValueCollection lookupValues)
            where TEntity : IEntity
        {
            if (lookupValues == null || lookupValues.Count == 0) { return new TEntity[0]; }
            Validation.ArgumentNotNullOrEmpty(fieldName, "fieldName");

            // Check the multi lookup cache for this lookup
            if (MultiLookupCache.ContainsKey(fieldName))
            {
                return MultiLookupCache[fieldName].Cast<TEntity>();
            }

            // Get the list to which the lookup field points
            var list = GetLookupList(fieldName);

            List<TEntity> result = new List<TEntity>();
            lookupValues.ForEach(lookupValue => result.Add(ListItemMapper.GetEntity<TEntity>(lookupValue.LookupId, lookupValue.LookupValue, list)));
            
            // store the new entity in the lookup cache
            MultiLookupCache[fieldName] = result.Cast<IEntity>();
            return result;
        }

        /// <summary>
        /// Helper method to get the <see cref="SPList"/> that a lookup field points to.
        /// </summary>
        /// <param name="fieldName">The name of the lookup field</param>
        /// <returns></returns>
        private SPList GetLookupList(string fieldName)
        {
            // get the field collection 
            var fields = ListItem != null ? ListItem.Fields : List.Fields;
            return GetLookupList(fields.GetFieldByInternalName(fieldName) as SPFieldLookup);
        }

        /// <summary>
        /// Helper method to get the <see cref="SPList"/> that a lookup field points to.
        /// </summary>
        /// <param name="field">the LookupField</param>
        /// <returns></returns>
        private SPList GetLookupList(SPFieldLookup field)
        {
            Validation.ArgumentNotNull(field, "field");

            // get the SPWeb object of the lookup list
            Guid lookupWebId = field.LookupWebId;
            if (lookupWebId == _web.ID) return _web.Lists[new Guid(field.LookupList)];
            // webs of the current listitem and the one of the lookup item are different
            // TODO: This will create a new SPWeb when it accesses the web and never disposes it!
            if (ListItem != null)
            {
                using (SPWeb lookupListWeb = ListItem.Web.Site.OpenWeb(lookupWebId))
                {
                    return lookupListWeb.Lists[new Guid(field.LookupList)];
                }
            }
            else
            {
                using (SPSite site = new SPSite(_eventProperties.SiteId))
                {
                    using (SPWeb lookupListWeb = site.OpenWeb(lookupWebId))
                    {
                        return lookupListWeb.Lists[new Guid(field.LookupList)];
                    }
                }
            }
        }

        /// <summary>
        /// Sets the value of a lookup field.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity to assign</typeparam>
        /// <param name="fieldName">The name of the lookup field</param>
        /// <param name="entity">The entity which will be assigned to the lookup field</param>
        protected internal void SetLookup<TEntity>(string fieldName, TEntity entity)
            where TEntity : IEntity
        {
            if (entity == null)
            { // clear the lookup field and the lookup cache
                Set(fieldName, "");
                LookupCache.Remove(fieldName);
            }
            else
            { // set the lookup field and the lookup cache
                Set(fieldName, new SPFieldLookupValue(entity.Id, entity.Title));
                LookupCache[fieldName] = entity;
            }
        }

        /// <summary>
        /// Sets the value of a multi-lookup field.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entities to assign</typeparam>
        /// <param name="fieldName">The name of the multi-lookup field</param>
        /// <param name="entities">The collection of entities which will be assigned to the multi-lookup field</param>
        protected internal void SetMultiLookups<TEntity>(string fieldName, IEnumerable<TEntity> entities)
            where TEntity : IEntity
        {
            var collection = new SPFieldLookupValueCollection();
            if (entities != null && entities.Count() > 0)
            { // set the multi-lookup field and the multi-lookup cache
                collection.AddRange(entities.Select(e => new SPFieldLookupValue(e.Id, e.Title)));
                MultiLookupCache[fieldName] = entities.Cast<IEntity>();
            }
            else
            { // clear the multi-lookup cache
                MultiLookupCache.Remove(fieldName);
            }
            Set(fieldName, collection);
        }
        
        #region Inverse Lookups
        /// <summary>
        /// Tries to find items in a given list where a lookup field points to this item.
        /// </summary>
        /// <remarks>
        /// For example an item employee has a lookup department. Use this method to get all employees of the department item
        /// </remarks>
        /// <typeparam name="TEntity">The type of the item that has the lookup to this item</typeparam>
        /// <param name="listUrl">The url of the list that has the lookup field</param>
        /// <param name="lookupField">The lookup field that points to this item's list</param>
        /// <returns></returns>
        protected internal IEnumerable<TEntity> GetInverseLookups<TEntity>(string listUrl, string lookupField)
            where TEntity : IEntity
        {
            return GetInverseLookups<TEntity>(listUrl, lookupField, SPViewScope.Default);
        }

        /// <summary>
        /// Tries to find items in a given list where a lookup field points to this item.
        /// </summary>
        /// <remarks>
        /// For example an item employee has a lookup department. Use this method to get all employees of the department item
        /// </remarks>
        /// <typeparam name="TEntity">The type of the item that has the lookup to this item</typeparam>
        /// <param name="listUrl">The url of the list that has the lookup field</param>
        /// <param name="lookupField">The lookup field that points to this item's list</param>
        /// <param name="scope">The scope of the query that searches the list represented by <paramref name="listUrl"/></param>
        /// <returns></returns>
        protected internal IEnumerable<TEntity> GetInverseLookups<TEntity>(string listUrl, string lookupField, SPViewScope scope)
            where TEntity : IEntity
        {
            Validation.ArgumentNotNullOrEmpty(listUrl, "listUrl");
            
            return GetInverseLookups<TEntity>(new ListRepository(listUrl, _web), lookupField, scope);
        }

        /// <summary>
        /// Tries to find items in a given list where a lookup field points to this item.
        /// </summary>
        /// <remarks>
        /// For example an item employee has a lookup department. Use this method to get all employees of the department item
        /// </remarks>
        /// <typeparam name="TEntity">The type of the item that has the lookup to this item</typeparam>
        /// <param name="list">The list that has the lookup field</param>
        /// <param name="lookupField">The lookup field that points to this item's list</param>
        /// <returns></returns>
        protected internal IEnumerable<TEntity> GetInverseLookups<TEntity>(ListRepository list, string lookupField)
            where TEntity : IEntity
        {
            return GetInverseLookups<TEntity>(list, lookupField, SPViewScope.Default);
        }

        /// <summary>
        /// Tries to find items in a given list where a lookup field points to this item.
        /// </summary>
        /// <remarks>
        /// For example an item employee has a lookup department. Use this method to get all employees of the department item
        /// </remarks>
        /// <typeparam name="TEntity">The type of the item that has the lookup to this item</typeparam>
        /// <param name="list">The list that has the lookup field</param>
        /// <param name="lookupField">The lookup field that points to this item's list</param>
        /// <param name="scope">The scope of the query that searches the <paramref name="list"/></param>
        /// <returns></returns>
        protected internal IEnumerable<TEntity> GetInverseLookups<TEntity>(ListRepository list, string lookupField, SPViewScope scope)
            where TEntity : IEntity
        {
            Validation.ArgumentNotNullOrEmpty(lookupField, "lookupField");
            Validation.ArgumentNotNull(list, "list");

            var key = string.Format(INVERSE_LOOKUP_KEY, list.List.RootFolder.Url, lookupField);
            if (MultiLookupCache.ContainsKey(key))
            {
                return MultiLookupCache[key].Cast<TEntity>();
            }
            var entities = list.GetEntities<TEntity>(new SPQuery().Where(QB.EqLookup(lookupField, Id)).Scope(scope));
            MultiLookupCache.Add(key, entities.Cast<IEntity>());
            return entities;
        }
        #endregion

        #region User or Groups
        /// <summary>
        /// Returns a <see cref="SPPrincipal"/> object of an user field.
        /// </summary>
        /// <param name="fieldName">The name of the field</param>
        /// <returns></returns>
        protected SPPrincipal GetUserOrGroup(string fieldName)
        {
            var fieldValue = new SPFieldUserValue(_web, Get<string>(fieldName));
            return EnsureUser(fieldValue, _web);
        }

        /// <summary>
        /// Returns a collection of <see cref="SPPrincipal"/> objects of an user field.
        /// </summary>
        /// <param name="fieldName">The name of the field</param>
        /// <returns></returns>
        protected IEnumerable<SPPrincipal> GetUserOrGroupMulti(string fieldName)
        {
            var retUsers = new List<SPPrincipal>();
            foreach (SPFieldUserValue fieldValue in new SPFieldUserValueCollection(_web, Get<string>(fieldName)))
            {
                SPPrincipal u = EnsureUser(fieldValue, _web);
                if (u != null) retUsers.Add(u);
            }
            return retUsers;
        }

        // TODO: write doc
        private SPPrincipal EnsureUser(SPFieldUserValue fieldValue, SPWeb web)
        {
            if ((fieldValue == null) || (fieldValue.LookupValue == null)) return null;

            Validation.ArgumentNotNull(web, "web");
            if ((fieldValue.LookupId == -1) && (!string.IsNullOrEmpty(fieldValue.LookupValue)))
            {
                //ensure user
                Permissions.RunWithElevatedPrivileges(web.Site.ID, web.ID, null, delegate(SPWeb pWeb, SPListItem Item)
                {
                    pWeb.AllowUnsafeUpdates = true;
                    pWeb.EnsureUser(fieldValue.LookupValue);
                });
                return web.SiteUsers[fieldValue.LookupValue];
            }
            else
            {
                return (fieldValue.User != null) ? (SPPrincipal)fieldValue.User : (SPPrincipal)web.SiteGroups[fieldValue.LookupValue];
            }
        }

        /// <summary>
        /// Sets the value of an user field
        /// </summary>
        /// <param name="fieldName">The name of the field</param>
        /// <param name="principal">The user or group to assign to the field</param>
        protected void SetUserOrGroup(string fieldName, SPPrincipal principal)
        {
            Set(fieldName, principal != null ? new SPFieldUserValue(_web, principal.ID, principal.Name).ToString() : string.Empty);
        }

        /// <summary>
        /// Sets the value of a multi-user field
        /// </summary>
        /// <param name="fieldName">The name of the field</param>
        /// <param name="principals">A collection of users or groups to assign to the field</param>
        protected void SetUserOrGroupMulti(string fieldName, IEnumerable<SPPrincipal> principals)
        {
            if (principals == null)
            {
                Set(fieldName, string.Empty);
                return;
            }
            SPFieldUserValueCollection uvc = new SPFieldUserValueCollection();
            foreach (SPPrincipal p in principals)
            {
                uvc.Add(new SPFieldUserValue(_web, p.ID, p.Name));
            }
            Set(fieldName, uvc.ToString());
        }
        #endregion

        #endregion

        #region Children

        /// <summary>
        /// Gets all children of this entity.
        /// </summary>
        /// <returns></returns>
        public IEnumerable<IEntity> GetChildren()
        {
            return GetChildren(new SPQuery());
        }

        /// <summary>
        /// Gets all children of this entity in a certain type.
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <returns></returns>
        public IEnumerable<TEntity> GetChildren<TEntity>()
            where TEntity : IEntity
        {
            return GetChildren<TEntity>(new SPQuery());
        }

        /// <summary>
        /// Gets all children of a certain content type.
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="cTypeId"></param>
        /// <returns></returns>
        public IEnumerable<TEntity> GetChildren<TEntity>(SPContentTypeId cTypeId)
            where TEntity : IEntity
        {
            return GetChildren<TEntity>(new SPQuery().Where(QB.IsContentType(cTypeId)));
        }

        /// <summary>
        /// Gets all children returned by a query in a certain type.
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="query"></param>
        /// <returns></returns>
        public IEnumerable<TEntity> GetChildren<TEntity>(SPQuery query)
            where TEntity : IEntity
        {
            query.Folder = ListItem.Folder;
            return List.GetItems(query).Cast<SPListItem>().Select(l => ListItemMapper.GetEntity<TEntity>(l));
        }

        /// <summary>
        /// Gets all children returned by a query.
        /// </summary>
        /// <param name="query"></param>
        /// <returns></returns>
        public IEnumerable<IEntity> GetChildren(SPQuery query)
        {
            query.Folder = ListItem.Folder;
            return List.GetItems(query).Cast<SPListItem>().Select(l => ListItemMapper.GetEntity(l, GetType().Assembly));
        }

        #endregion

        #region Updates/Deletes
        /// <summary>
        /// Updates the entity. EVENTS DISABLED!
        /// </summary>
        public virtual void Update()
        {
            Update(false);
        }

        /// <summary>
        /// Updates the entity.
        /// </summary>
        /// <param name="enableEvents">true, to enable eventfiring; otherwise false</param>
        public virtual void Update(bool enableEvents)
        {
            if (!IsSynchronous || _eventProperties.EventType == SPEventReceiverType.ItemDeleting)
            {
                try
                {
                    EventReceiverTool.SetEventFiring(enableEvents);
                    ListItem.Update();
                }
                finally
                {
                    EventReceiverTool.EnableEventFiring();
                }
            }
        }

        /// <summary>
        /// Updates the entity without changing the editor. EVENTS DISABLED!
        /// </summary>
        public virtual void SystemUpdate()
        {
            SystemUpdate(false);
        }

        /// <summary>
        /// Updates the entity without changing the editor.
        /// </summary>
        /// <param name="enableEvents">true, to enable eventfiring; otherwise false</param>
        public virtual void SystemUpdate(bool enableEvents)
        {
            SystemUpdate(enableEvents, false);
        }

        /// <summary>
        /// Updates the entity without changing the editor.
        /// </summary>
        /// <param name="enableEvents">true, to enable eventfiring; otherwise false</param>
        /// <param name="incrementListItemVersion"></param>
        public virtual void SystemUpdate(bool enableEvents, bool incrementListItemVersion)
        {
            if (!IsSynchronous)
            {
                try
                {
                    EventReceiverTool.SetEventFiring(enableEvents);
                    ListItem.SystemUpdate(incrementListItemVersion);
                }
                finally
                {
                    EventReceiverTool.EnableEventFiring();
                }
            }

        }

        /// <summary>
        /// Deletes the entity. EVENTS DISABLED!
        /// </summary>
        public virtual void Delete()
        {
            Delete(false);
        }

        /// <summary>
        /// Deletes the entity.
        /// </summary>
        /// <param name="enableEvents">true, to enable eventfiring; otherwise false</param>
        public virtual void Delete(bool enableEvents)
        {
            Delete(false, enableEvents);
        }

        /// <summary>
        /// Moves the entity into the recycle bin. EVENTS DISABLED!
        /// </summary>
        public virtual void Recycle()
        {
            Recycle(false);
        }

        /// <summary>
        /// Moves the entity into the recycle bin.
        /// </summary>
        /// <param name="enableEvents">true, to enable eventfiring; otherwise false</param>
        public virtual void Recycle(bool enableEvents)
        {
            Delete(true, enableEvents);
        }

        /// <summary>
        /// Deletes or moves the entity into the recycle bin.
        /// </summary>
        /// <param name="moveToRecycleBin">true, to move the entity into the recycle bin; false to delete it</param>
        /// <param name="enableEvents">true, to enable eventfiring; otherwise false</param>
        private void Delete(bool moveToRecycleBin, bool enableEvents)
        {
            try
            {
                EventReceiverTool.SetEventFiring(enableEvents);
                if (moveToRecycleBin) ListItem.Recycle();
                else ListItem.Delete();
            }
            finally
            {
                EventReceiverTool.EnableEventFiring();
            }
        }

        public TEntity CopyTo<TEntity>(IListRepository destination)
            where TEntity:IEntity
        {
            var result = destination.Create<TEntity>(this.ContentTypeId);

            foreach (SPField field in result.ListItem.Fields.Cast<SPField>().Where(f => !f.ReadOnlyField))
            {
                if (field.InternalName == "Attachments") { continue; }
                result.ListItem[field.InternalName] = this.ListItem[field.InternalName];
            }
                  

            return result;
        }
        #endregion

        #region Security Methods

        /// <summary>
        /// Runs a code block in elevated privileges and passes this entity in an elevated context.
        /// </summary>
        /// <remarks>
        /// The passed entity is a different instance, so don't assume, that your changes in the privileged entity will come through to the original entity.
        /// </remarks>
        /// <typeparam name="TEntity">The type of the entity</typeparam>
        /// <param name="privilegedCode"></param>
        public void RunWithElevatedPrivileges<TEntity>(Action<TEntity> privilegedCode)
        {
            if (_web == null || _list == null) { throw new InvalidOperationException("You have to provide _list and _web first."); }

            Permissions.RunWithElevatedPrivileges(_web.Site.ID, _web.ID, _list.ID, Id, (pItem) =>
            {
                privilegedCode(ListItemMapper.GetEntity<TEntity>(pItem));
            });
        }

        #endregion

        #region Static Helper Methods
        
        #endregion
    }
}
